๊ณ ๊ธ ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ ๋น๋์ค ์ฒ๋ฆฌ๋ฅผ ๊ฒฝํํด๋ณด์ธ์. WebCodecs API๋ก ์์ VideoFrame ํ๋ฉด ๋ฐ์ดํฐ์ ์ง์ ์ ๊ทผํ๊ณ ์กฐ์ํ์ฌ ์ปค์คํ ํจ๊ณผ ๋ฐ ๋ถ์์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ฐฐ์๋ณด์ธ์.
WebCodecs VideoFrame ํ๋ฉด ์ ๊ทผ: ์์ ๋น๋์ค ๋ฐ์ดํฐ ์กฐ์ ์ฌ์ธต ๋ถ์
์๋
๊ฐ ์น ๋ธ๋ผ์ฐ์ ์์์ ๊ณ ์ฑ๋ฅ ๋น๋์ค ์ฒ๋ฆฌ๋ ๋จผ ๊ฟ์ฒ๋ผ ๋๊ปด์ก์ต๋๋ค. ๊ฐ๋ฐ์๋ค์ ์ข
์ข
<video> ์์์ 2D Canvas API์ ํ๊ณ์ ๊ฐํ ์์๋๋ฐ, ์ด ๊ธฐ์ ๋ค์ ๊ฐ๋ ฅํ๊ธด ํ์ง๋ง ์ฑ๋ฅ ๋ณ๋ชฉ ํ์์ ์ ๋ฐํ๊ณ ๊ธฐ์ ์ ์์ ๋น๋์ค ๋ฐ์ดํฐ์ ๋ํ ์ ๊ทผ์ ์ ํํ์ต๋๋ค. WebCodecs API์ ๋ฑ์ฅ์ ์ด๋ฌํ ํ๊ฒฝ์ ๊ทผ๋ณธ์ ์ผ๋ก ๋ฐ๊พธ์ด, ๋ธ๋ผ์ฐ์ ์ ๋ด์ฅ๋ ๋ฏธ๋์ด ์ฝ๋ฑ์ ๋ํ ์ ์์ค ์ ๊ทผ์ ์ ๊ณตํฉ๋๋ค. ์ด API์ ๊ฐ์ฅ ํ์ ์ ์ธ ๊ธฐ๋ฅ ์ค ํ๋๋ VideoFrame ๊ฐ์ฒด๋ฅผ ํตํด ๊ฐ๋ณ ๋น๋์ค ํ๋ ์์ ์์ ๋ฐ์ดํฐ์ ์ง์ ์ ๊ทผํ๊ณ ์กฐ์ํ ์ ์๋ ๋ฅ๋ ฅ์
๋๋ค.
์ด ๊ธ์ ๋จ์ํ ๋น๋์ค ์ฌ์์ ๋์ด ๋ ๋์๊ฐ๊ณ ์ ํ๋ ๊ฐ๋ฐ์๋ค์ ์ํ ์ข
ํฉ ๊ฐ์ด๋์
๋๋ค. ์ฐ๋ฆฌ๋ VideoFrame ํ๋ฉด ์ ๊ทผ์ ๋ณต์ก์ฑ์ ํ๊ตฌํ๊ณ , ์ ๊ณต๊ฐ ๋ฐ ๋ฉ๋ชจ๋ฆฌ ๋ ์ด์์๊ณผ ๊ฐ์ ๊ฐ๋
์ ๋ช
ํํ ์ค๋ช
ํ๋ฉฐ, ์ค์๊ฐ ํํฐ๋ถํฐ ์ ๊ตํ ์ปดํจํฐ ๋น์ ์์
์ ์ด๋ฅด๊ธฐ๊น์ง ์ฐจ์ธ๋ ์ธ๋ธ๋ผ์ฐ์ ๋น๋์ค ์ ํ๋ฆฌ์ผ์ด์
์ ๊ตฌ์ถํ ์ ์๋๋ก ์ค์ฉ์ ์ธ ์์ ๋ฅผ ์ ๊ณตํ ๊ฒ์
๋๋ค.
์ฌ์ ์๊ตฌ ์ฌํญ
์ด ๊ฐ์ด๋๋ฅผ ์ต๋ํ ํ์ฉํ๋ ค๋ฉด ๋ค์์ ๋ํ ํ์คํ ์ดํด๊ฐ ํ์ํฉ๋๋ค:
- ์ต์ ์๋ฐ์คํฌ๋ฆฝํธ: ๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ(
async/await, Promises)์ ํฌํจํฉ๋๋ค. - ๊ธฐ๋ณธ์ ์ธ ๋น๋์ค ๊ฐ๋ : ํ๋ ์, ํด์๋, ์ฝ๋ฑ๊ณผ ๊ฐ์ ์ฉ์ด์ ์ต์ํ๋ฉด ๋์์ด ๋ฉ๋๋ค.
- ๋ธ๋ผ์ฐ์ API: Canvas 2D๋ WebGL๊ณผ ๊ฐ์ API ์ฌ์ฉ ๊ฒฝํ์ด ์์ผ๋ฉด ์ ์ฉํ์ง๋ง, ํ์ ์ฌํญ์ ์๋๋๋ค.
๋น๋์ค ํ๋ ์, ์ ๊ณต๊ฐ, ๊ทธ๋ฆฌ๊ณ ํ๋ฉด ์ดํดํ๊ธฐ
API์ ๋ํด ์์๋ณด๊ธฐ ์ ์, ๋น๋์ค ํ๋ ์ ๋ฐ์ดํฐ๊ฐ ์ค์ ๋ก ์ด๋ป๊ฒ ์๊ฒผ๋์ง์ ๋ํ ๊ฒฌ๊ณ ํ ์ ์ ๋ชจ๋ธ์ ๊ตฌ์ถํด์ผ ํฉ๋๋ค. ๋์งํธ ๋น๋์ค๋ ์ ์ง๋ ์ด๋ฏธ์ง, ์ฆ ํ๋ ์์ ์ฐ์์ ๋๋ค. ๊ฐ ํ๋ ์์ ํฝ์ ์ ๊ทธ๋ฆฌ๋์ด๋ฉฐ, ๊ฐ ํฝ์ ์ ์์์ ๊ฐ์ง๋๋ค. ๊ทธ ์์์ด ์ด๋ป๊ฒ ์ ์ฅ๋๋์ง๋ ์ ๊ณต๊ฐ๊ณผ ํฝ์ ํ์์ ์ํด ์ ์๋ฉ๋๋ค.
RGBA: ์น์ ๊ธฐ๋ณธ ์ธ์ด
๋๋ถ๋ถ์ ์น ๊ฐ๋ฐ์๋ค์ RGBA ์์ ๋ชจ๋ธ์ ์ต์ํฉ๋๋ค. ๊ฐ ํฝ์ ์ ๋นจ๊ฐ(Red), ์ด๋ก(Green), ํ๋(Blue), ๊ทธ๋ฆฌ๊ณ ์ํ(Alpha, ํฌ๋ช ๋)์ ๋ค ๊ฐ์ง ๊ตฌ์ฑ ์์๋ก ํํ๋ฉ๋๋ค. ๋ฐ์ดํฐ๋ ์ผ๋ฐ์ ์ผ๋ก ๋ฉ๋ชจ๋ฆฌ์ ์ธํฐ๋ฆฌ๋ธ(interleaved) ๋ฐฉ์์ผ๋ก ์ ์ฅ๋๋๋ฐ, ์ด๋ ๋จ์ผ ํฝ์ ์ ๋ํ R, G, B, A ๊ฐ์ด ์ฐ์์ ์ผ๋ก ์ ์ฅ๋๋ค๋ ์๋ฏธ์ ๋๋ค:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
์ด ๋ชจ๋ธ์์ ์ ์ฒด ์ด๋ฏธ์ง๋ ๋จ์ผ์ ์ฐ์์ ์ธ ๋ฉ๋ชจ๋ฆฌ ๋ธ๋ก์ ์ ์ฅ๋ฉ๋๋ค. ์ด๋ฅผ ๋จ์ผ "ํ๋ฉด(plane)"์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ๋ ๊ฒ์ผ๋ก ์๊ฐํ ์ ์์ต๋๋ค.
YUV: ๋น๋์ค ์์ถ์ ์ธ์ด
๊ทธ๋ฌ๋ ๋น๋์ค ์ฝ๋ฑ์ ๊ฑฐ์ RGBA๋ฅผ ์ง์ ๋ค๋ฃจ์ง ์์ต๋๋ค. ๊ทธ๋ค์ YUV(๋ ์ ํํ๊ฒ๋ Y'CbCr) ์ ๊ณต๊ฐ์ ์ ํธํฉ๋๋ค. ์ด ๋ชจ๋ธ์ ์ด๋ฏธ์ง ์ ๋ณด๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ๋ถ๋ฆฌํฉ๋๋ค:
- Y (ํ๋): ๋ฐ๊ธฐ ๋๋ ํ๋ฐฑ ์ ๋ณด. ์ฌ๋์ ๋์ ํ๋ ๋ณํ์ ๊ฐ์ฅ ๋ฏผ๊ฐํฉ๋๋ค.
- U (Cb) ๋ฐ V (Cr): ์์ฐจ ๋๋ ์์-์ฐจ์ด ์ ๋ณด. ์ฌ๋์ ๋์ ๋ฐ๊ธฐ ๋ํ ์ผ๋ณด๋ค ์์ ๋ํ ์ผ์ ๋ ๋ฏผ๊ฐํฉ๋๋ค.
์ด๋ฌํ ๋ถ๋ฆฌ๋ ํจ์จ์ ์ธ ์์ถ์ ํต์ฌ์ ๋๋ค. U์ V ๊ตฌ์ฑ ์์์ ํด์๋๋ฅผ ์ค์ด๋ ๊ธฐ์ ์ธ ํฌ๋ก๋ง ์๋ธ์ํ๋ง(chroma subsampling)์ ํตํด, ์ฐ๋ฆฌ๋ ์ธ์งํ ์ ์๋ ํ์ง ์์ค์ ์ต์ํํ๋ฉด์ ํ์ผ ํฌ๊ธฐ๋ฅผ ํฌ๊ฒ ์ค์ผ ์ ์์ต๋๋ค. ์ด๋ Y, U, V ๊ตฌ์ฑ ์์๊ฐ ๋ณ๋์ ๋ฉ๋ชจ๋ฆฌ ๋ธ๋ก, ์ฆ "ํ๋ฉด"์ ์ ์ฅ๋๋ ํ๋ฉด(planar) ํฝ์ ํ์์ผ๋ก ์ด์ด์ง๋๋ค.
์ผ๋ฐ์ ์ธ ํ์์ I420(YUV 4:2:0์ ํ ์ข ๋ฅ)์ผ๋ก, 2x2 ํฝ์ ๋ธ๋ก๋ง๋ค 4๊ฐ์ Y ์ํ์ด ์์ง๋ง U์ V ์ํ์ ๊ฐ๊ฐ ํ๋์ฉ๋ง ์์ต๋๋ค. ์ด๋ U์ V ํ๋ฉด์ด Y ํ๋ฉด์ ๋๋น์ ๋์ด์ ์ ๋ฐ์ ๊ฐ์ง์ ์๋ฏธํฉ๋๋ค.
์ด ์ฐจ์ด์ ์ ์ดํดํ๋ ๊ฒ์ ๋งค์ฐ ์ค์ํฉ๋๋ค. ์๋ํ๋ฉด WebCodecs๋ ๋์ฝ๋๊ฐ ์ ๊ณตํ๋ ๋ฐ๋ก ์ด ํ๋ฉด๋ค์ ๋ํ ์ง์ ์ ์ธ ์ ๊ทผ์ ์ ๊ณตํ๊ธฐ ๋๋ฌธ์ ๋๋ค.
VideoFrame ๊ฐ์ฒด: ํฝ์
๋ฐ์ดํฐ๋ก์ ๊ด๋ฌธ
์ด ํผ์ฆ์ ์ค์ฌ ์กฐ๊ฐ์ VideoFrame ๊ฐ์ฒด์
๋๋ค. ์ด๊ฒ์ ๋น๋์ค์ ๋จ์ผ ํ๋ ์์ ๋ํ๋ด๋ฉฐ ํฝ์
๋ฐ์ดํฐ๋ฟ๋ง ์๋๋ผ ์ค์ํ ๋ฉํ๋ฐ์ดํฐ๋ ํฌํจํฉ๋๋ค.
VideoFrame์ ์ฃผ์ ์์ฑ
format: ํฝ์ ํ์(์: 'I420', 'NV12', 'RGBA')์ ๋ํ๋ด๋ ๋ฌธ์์ด์ ๋๋ค.codedWidth/codedHeight: ์ฝ๋ฑ์์ ์๊ตฌํ๋ ํจ๋ฉ์ ํฌํจํ์ฌ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ๋ ํ๋ ์์ ์ ์ฒด ํฌ๊ธฐ์ ๋๋ค.displayWidth/displayHeight: ํ๋ ์์ ํ์ํ๋ ๋ฐ ์ฌ์ฉํด์ผ ํ๋ ํฌ๊ธฐ์ ๋๋ค.timestamp: ๋ง์ดํฌ๋ก์ด ๋จ์์ ํ๋ ์ ํ์ ํ์์คํฌํ์ ๋๋ค.duration: ๋ง์ดํฌ๋ก์ด ๋จ์์ ํ๋ ์ ์ง์ ์๊ฐ์ ๋๋ค.
๋ง๋ฒ์ ๋ฉ์๋: copyTo()
์์ ํฝ์
๋ฐ์ดํฐ์ ์ ๊ทผํ๋ ์ฃผ์ ๋ฉ์๋๋ videoFrame.copyTo(destination, options)์
๋๋ค. ์ด ๋น๋๊ธฐ ๋ฉ์๋๋ ํ๋ ์์ ํ๋ฉด ๋ฐ์ดํฐ๋ฅผ ์ฌ๋ฌ๋ถ์ด ์ ๊ณตํ๋ ๋ฒํผ๋ก ๋ณต์ฌํฉ๋๋ค.
destination: ๋ฐ์ดํฐ๋ฅผ ๋ด์ ์ ์์ ๋งํผ ์ถฉ๋ถํ ํฐArrayBuffer๋๋ ํ์ ๋ฐฐ์ด(์:Uint8Array)์ ๋๋ค.options: ๋ณต์ฌํ ํ๋ฉด๊ณผ ๊ทธ ๋ฉ๋ชจ๋ฆฌ ๋ ์ด์์์ ์ง์ ํ๋ ๊ฐ์ฒด์ ๋๋ค. ์๋ตํ๋ฉด ๋ชจ๋ ํ๋ฉด์ ๋จ์ผ์ ์ฐ์์ ์ธ ๋ฒํผ์ ๋ณต์ฌํฉ๋๋ค.
์ด ๋ฉ์๋๋ ํ๋ ์์ ๊ฐ ํ๋ฉด์ ๋ํด ํ๋์ฉ, PlaneLayout ๊ฐ์ฒด์ ๋ฐฐ์ด๋ก ํด์๋๋ Promise๋ฅผ ๋ฐํํฉ๋๋ค. ๊ฐ PlaneLayout ๊ฐ์ฒด๋ ๋ ๊ฐ์ง ์ค์ํ ์ ๋ณด๋ฅผ ํฌํจํฉ๋๋ค:
offset: ์ด ํ๋ฉด์ ๋ฐ์ดํฐ๊ฐ ๋์ ๋ฒํผ ๋ด์์ ์์๋๋ ๋ฐ์ดํธ ์คํ์ ์ ๋๋ค.stride: ํด๋น ํ๋ฉด์์ ํ ํฝ์ ํ์ ์์๋ถํฐ ๋ค์ ํ์ ์์๊น์ง์ ๋ฐ์ดํธ ์์ ๋๋ค.
์ค์ ๊ฐ๋ : Stride ๋ Width
์ด๊ฒ์ ์ ์์ค ๊ทธ๋ํฝ ํ๋ก๊ทธ๋๋ฐ์ ์ฒ์ ์ ํ๋ ๊ฐ๋ฐ์๋ค์ด ๊ฐ์ฅ ํํ๊ฒ ๊ฒช๋ ํผ๋์ ์์ธ ์ค ํ๋์ ๋๋ค. ๊ฐ ํฝ์ ๋ฐ์ดํฐ ํ์ด ์๋ก ๋ฐ๋ก ๋ค์ ๊ฝ ์ฑ์์ ธ ์๋ค๊ณ ๊ฐ์ ํ ์ ์์ต๋๋ค.
- Width๋ ์ด๋ฏธ์ง ํ ํ์ ์๋ ํฝ์ ์ ์์ ๋๋ค.
- Stride(ํผ์น ๋๋ ๋ผ์ธ ์คํ ์ด๋ผ๊ณ ๋ ํจ)๋ ๋ฉ๋ชจ๋ฆฌ์์ ํ ํ์ ์์๋ถํฐ ๋ค์ ํ์ ์์๊น์ง์ ๋ฐ์ดํธ ์์ ๋๋ค.
์ข
์ข
stride๋ width * bytes_per_pixel๋ณด๋ค ํฝ๋๋ค. ์ด๋ CPU๋ GPU์ ์ํ ๋ ๋น ๋ฅธ ์ฒ๋ฆฌ๋ฅผ ์ํด ๋ฉ๋ชจ๋ฆฌ๊ฐ ํ๋์จ์ด ๊ฒฝ๊ณ(์: 32 ๋๋ 64๋ฐ์ดํธ ๊ฒฝ๊ณ)์ ๋ง์ถฐ ํจ๋ฉ๋๋ ๊ฒฝ์ฐ๊ฐ ๋ง๊ธฐ ๋๋ฌธ์
๋๋ค. ํน์ ํ์ ํฝ์
๋ฉ๋ชจ๋ฆฌ ์ฃผ์๋ฅผ ๊ณ์ฐํ ๋๋ ํญ์ ์คํธ๋ผ์ด๋๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
์คํธ๋ผ์ด๋๋ฅผ ๋ฌด์ํ๋ฉด ์ด๋ฏธ์ง๊ฐ ๊ธฐ์ธ์ด์ง๊ฑฐ๋ ์๊ณก๋๊ณ ์๋ชป๋ ๋ฐ์ดํฐ ์ ๊ทผ์ผ๋ก ์ด์ด์ง๋๋ค.
์ค์ฉ ์์ 1: ๊ทธ๋ ์ด์ค์ผ์ผ ํ๋ฉด ์ ๊ทผ ๋ฐ ํ์ํ๊ธฐ
๊ฐ๋จํ์ง๋ง ๊ฐ๋ ฅํ ์์ ๋ก ์์ํ๊ฒ ์ต๋๋ค. ์น์์ ๋๋ถ๋ถ์ ๋น๋์ค๋ I420๊ณผ ๊ฐ์ YUV ํ์์ผ๋ก ์ธ์ฝ๋ฉ๋ฉ๋๋ค. 'Y' ํ๋ฉด์ ์ฌ์ค์ ์ด๋ฏธ์ง์ ์์ ํ ๊ทธ๋ ์ด์ค์ผ์ผ ํํ์ ๋๋ค. ์ฐ๋ฆฌ๋ ์ด ํ๋ฉด๋ง ์ถ์ถํ์ฌ ์บ๋ฒ์ค์ ๋ ๋๋งํ ์ ์์ต๋๋ค.
async function displayGrayscale(videoFrame) {
// videoFrame์ด 'I420' ๋๋ 'NV12'์ ๊ฐ์ YUV ํ์์ด๋ผ๊ณ ๊ฐ์ ํฉ๋๋ค.
if (!videoFrame.format.startsWith('I4')) {
console.error('์ด ์์ ๋ YUV 4:2:0 ํ๋ฉด ํ์์ด ํ์ํฉ๋๋ค.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // Y ํ๋ฉด์ ํญ์ ์ฒซ ๋ฒ์งธ์
๋๋ค.
// Y ํ๋ฉด ๋ฐ์ดํฐ๋ง ๋ด์ ๋ฒํผ๋ฅผ ์์ฑํฉ๋๋ค.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// Y ํ๋ฉด์ ์ฐ๋ฆฌ ๋ฒํผ๋ก ๋ณต์ฌํฉ๋๋ค.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// ์ด์ yPlaneData์๋ ์์ ๊ทธ๋ ์ด์ค์ผ์ผ ํฝ์
์ด ํฌํจ๋ฉ๋๋ค.
// ๋ ๋๋ง์ด ํ์ํฉ๋๋ค. ์บ๋ฒ์ค๋ฅผ ์ํ RGBA ๋ฒํผ๋ฅผ ์์ฑํ ๊ฒ์
๋๋ค.
const canvas = document.getElementById('my-canvas');
canvas.width = videoFrame.displayWidth;
canvas.height = videoFrame.displayHeight;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
// ์บ๋ฒ์ค ํฝ์
์ ์ํํ๋ฉฐ Y ํ๋ฉด ๋ฐ์ดํฐ๋ก ์ฑ์๋๋ค.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// ์ค์: stride๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ๋ฐ๋ฅธ ์์ค ์ธ๋ฑ์ค๋ฅผ ์ฐพ์ผ์ธ์!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// RGBA ImageData ๋ฒํผ์ ๋์ ์ธ๋ฑ์ค๋ฅผ ๊ณ์ฐํฉ๋๋ค.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // ๋นจ๊ฐ
imageData.data[rgbaIndex + 1] = luma; // ์ด๋ก
imageData.data[rgbaIndex + 2] = luma; // ํ๋
imageData.data[rgbaIndex + 3] = 255; // ์ํ
}
}
ctx.putImageData(imageData, 0, 0);
// ์ค์: ๋ฉ๋ชจ๋ฆฌ๋ฅผ ํด์ ํ๋ ค๋ฉด ํญ์ VideoFrame์ ๋ซ์์ผ ํฉ๋๋ค.
videoFrame.close();
}
์ด ์์ ๋ ์ฌ๋ฐ๋ฅธ ํ๋ฉด ๋ ์ด์์ ์๋ณ, ๋์ ๋ฒํผ ํ ๋น, copyTo๋ฅผ ์ฌ์ฉํ ๋ฐ์ดํฐ ์ถ์ถ, ๊ทธ๋ฆฌ๊ณ stride๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ํํ์ฌ ์ ์ด๋ฏธ์ง๋ฅผ ๊ตฌ์ฑํ๋ ๋ช ๊ฐ์ง ํต์ฌ ๋จ๊ณ๋ฅผ ๋ณด์ฌ์ค๋๋ค.
์ค์ฉ ์์ 2: ์ง์ ์กฐ์ (์ธํผ์ ํํฐ)
์ด์ ์ง์ ๋ฐ์ดํฐ ์กฐ์์ ์ํํด ๋ณด๊ฒ ์ต๋๋ค. ์ธํผ์ ํํฐ๋ ๊ตฌํํ๊ธฐ ์ฌ์ด ๊ณ ์ ์ ์ธ ํจ๊ณผ์ ๋๋ค. ์ด ์์ ์์๋ ์บ๋ฒ์ค๋ WebGL ์ปจํ ์คํธ์์ ์ป์ ์ ์๋ RGBA ํ๋ ์์ผ๋ก ์์ ํ๋ ๊ฒ์ด ๋ ์ฝ์ต๋๋ค.
async function applySepiaFilter(videoFrame) {
// ์ด ์์ ๋ ์
๋ ฅ ํ๋ ์์ด 'RGBA' ๋๋ 'BGRA'๋ผ๊ณ ๊ฐ์ ํฉ๋๋ค.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('์ธํผ์ ํํฐ ์์ ๋ RGBA ํ๋ ์์ด ํ์ํฉ๋๋ค.');
videoFrame.close();
return null;
}
// ํฝ์
๋ฐ์ดํฐ๋ฅผ ๋ด์ ๋ฒํผ๋ฅผ ํ ๋นํฉ๋๋ค.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA๋ ๋จ์ผ ํ๋ฉด์
๋๋ค
// ์ด์ ๋ฒํผ์ ๋ฐ์ดํฐ๋ฅผ ์กฐ์ํฉ๋๋ค.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // ํฝ์
๋น 4๋ฐ์ดํธ (R,G,B,A)
const r = frameData[pixelIndex];
const g = frameData[pixelIndex + 1];
const b = frameData[pixelIndex + 2];
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
frameData[pixelIndex] = Math.min(255, tr);
frameData[pixelIndex + 1] = Math.min(255, tg);
frameData[pixelIndex + 2] = Math.min(255, tb);
// ์ํ(frameData[pixelIndex + 3])๋ ๋ณ๊ฒฝ๋์ง ์์ต๋๋ค.
}
}
// ์์ ๋ ๋ฐ์ดํฐ๋ก *์๋ก์ด* VideoFrame์ ์์ฑํฉ๋๋ค.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// ์๋ณธ ํ๋ ์์ ๋ซ๋ ๊ฒ์ ์์ง ๋ง์ธ์!
videoFrame.close();
return newFrame;
}
์ด๋ ์์ ํ ์ฝ๊ธฐ-์์ -์ฐ๊ธฐ ์ฃผ๊ธฐ๋ฅผ ๋ณด์ฌ์ค๋๋ค: ๋ฐ์ดํฐ๋ฅผ ๋ณต์ฌํ๊ณ , ์คํธ๋ผ์ด๋๋ฅผ ์ฌ์ฉํ์ฌ ์ํํ๊ณ , ๊ฐ ํฝ์
์ ์ํ์ ๋ณํ์ ์ ์ฉํ ๋ค์, ๊ฒฐ๊ณผ ๋ฐ์ดํฐ๋ก ์๋ก์ด VideoFrame์ ๊ตฌ์ฑํฉ๋๋ค. ์ด ์๋ก์ด ํ๋ ์์ ์บ๋ฒ์ค์ ๋ ๋๋งํ๊ฑฐ๋, VideoEncoder๋ก ๋ณด๋ด๊ฑฐ๋, ๋ค๋ฅธ ์ฒ๋ฆฌ ๋จ๊ณ๋ก ์ ๋ฌํ ์ ์์ต๋๋ค.
์ฑ๋ฅ ๋ฌธ์ : JavaScript ๋ WebAssembly (WASM)
JavaScript์์ ๋ชจ๋ ํ๋ ์์ ๋ํด ์๋ฐฑ๋ง ๊ฐ์ ํฝ์ (1080p ํ๋ ์์ 2๋ฐฑ๋ง ๊ฐ ์ด์์ ํฝ์ , ์ฆ RGBA์์ 8๋ฐฑ๋ง ๊ฐ์ ๋ฐ์ดํฐ ํฌ์ธํธ๋ฅผ ๊ฐ์ง)์ ๋ฐ๋ณตํ๋ ๊ฒ์ ๋๋ฆด ์ ์์ต๋๋ค. ์ต์ JS ์์ง์ด ๋งค์ฐ ๋น ๋ฅด๊ธด ํ์ง๋ง, ๊ณ ํด์๋ ๋น๋์ค(HD, 4K)์ ์ค์๊ฐ ์ฒ๋ฆฌ์๋ ์ด ์ ๊ทผ ๋ฐฉ์์ด ๋ฉ์ธ ์ค๋ ๋๋ฅผ ์ฝ๊ฒ ์๋ํ์ฌ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ํดํ ์ ์์ต๋๋ค.
๋ฐ๋ก ์ด ์ง์ ์์ WebAssembly (WASM)๊ฐ ํ์์ ์ธ ๋๊ตฌ๊ฐ ๋ฉ๋๋ค. WASM์ ์ฌ์ฉํ๋ฉด C++, Rust ๋๋ Go์ ๊ฐ์ ์ธ์ด๋ก ์์ฑ๋ ์ฝ๋๋ฅผ ๋ธ๋ผ์ฐ์ ๋ด์์ ๊ฑฐ์ ๋ค์ดํฐ๋ธ ์๋๋ก ์คํํ ์ ์์ต๋๋ค. ๋น๋์ค ์ฒ๋ฆฌ๋ฅผ ์ํ ์ํฌํ๋ก์ฐ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
- ์๋ฐ์คํฌ๋ฆฝํธ์์:
videoFrame.copyTo()๋ฅผ ์ฌ์ฉํ์ฌ ์์ ํฝ์ ๋ฐ์ดํฐ๋ฅผArrayBuffer๋ก ๊ฐ์ ธ์ต๋๋ค. - WASM์ผ๋ก ์ ๋ฌ: ์ปดํ์ผ๋ WASM ๋ชจ๋์ ์ด ๋ฒํผ์ ๋ํ ์ฐธ์กฐ๋ฅผ ์ ๋ฌํฉ๋๋ค. ์ด๋ ๋ฐ์ดํฐ๋ฅผ ๋ณต์ฌํ์ง ์๊ธฐ ๋๋ฌธ์ ๋งค์ฐ ๋น ๋ฅธ ์์ ์ ๋๋ค.
- WASM์์ (C++/Rust): ๊ณ ๋๋ก ์ต์ ํ๋ ์ด๋ฏธ์ง ์ฒ๋ฆฌ ์๊ณ ๋ฆฌ์ฆ์ ๋ฉ๋ชจ๋ฆฌ ๋ฒํผ์์ ์ง์ ์คํํฉ๋๋ค. ์ด๋ ์๋ฐ์คํฌ๋ฆฝํธ ๋ฃจํ๋ณด๋ค ๋ช ๋ฐฐ๋ ๋น ๋ฆ ๋๋ค.
- ์๋ฐ์คํฌ๋ฆฝํธ๋ก ๋ฐํ: WASM ์์
์ด ๋๋๋ฉด ์ ์ด๊ฐ ์๋ฐ์คํฌ๋ฆฝํธ๋ก ๋์์ต๋๋ค. ๊ทธ๋ฐ ๋ค์ ์์ ๋ ๋ฒํผ๋ฅผ ์ฌ์ฉํ์ฌ ์๋ก์ด
VideoFrame์ ์์ฑํ ์ ์์ต๋๋ค.
๊ฐ์ ๋ฐฐ๊ฒฝ, ๊ฐ์ฒด ๊ฐ์ง ๋๋ ๋ณต์กํ ํํฐ์ ๊ฐ์ ์ง์งํ ์ค์๊ฐ ๋น๋์ค ์กฐ์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฒฝ์ฐ, WebAssembly๋ฅผ ํ์ฉํ๋ ๊ฒ์ ์ ํ ์ฌํญ์ด ์๋๋ผ ํ์์ ๋๋ค.
๋ค์ํ ํฝ์ ํ์ ์ฒ๋ฆฌํ๊ธฐ (์: I420, NV12)
RGBA๋ ๊ฐ๋จํ์ง๋ง, VideoDecoder๋ก๋ถํฐ๋ ๋๋ถ๋ถ ํ๋ฉด YUV ํ์์ ํ๋ ์์ ๋ฐ๊ฒ ๋ ๊ฒ์
๋๋ค. I420๊ณผ ๊ฐ์ ์์ ํ๋ฉด ํ์์ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
I420 ํ์์ VideoFrame์ layout ๋ฐฐ์ด์ ์ธ ๊ฐ์ ๋ ์ด์์ ์ค๋ช
์๋ฅผ ๊ฐ์ง๋๋ค:
layout[0]: Y ํ๋ฉด (ํ๋). ํฌ๊ธฐ๋codedWidthxcodedHeight์ ๋๋ค.layout[1]: U ํ๋ฉด (์์ฐจ). ํฌ๊ธฐ๋codedWidth/2xcodedHeight/2์ ๋๋ค.layout[2]: V ํ๋ฉด (์์ฐจ). ํฌ๊ธฐ๋codedWidth/2xcodedHeight/2์ ๋๋ค.
์ธ ํ๋ฉด ๋ชจ๋๋ฅผ ๋จ์ผ ๋ฒํผ์ ๋ณต์ฌํ๋ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts๋ 3๊ฐ์ PlaneLayout ๊ฐ์ฒด ๋ฐฐ์ด์
๋๋ค
console.log('Y Plane Layout:', layouts[0]); // { offset: 0, stride: ... }
console.log('U Plane Layout:', layouts[1]); // { offset: ..., stride: ... }
console.log('V Plane Layout:', layouts[2]); // { offset: ..., stride: ... }
// ์ด์ `allPlanesData` ๋ฒํผ ๋ด์์ ๊ฐ ํ๋ฉด์ ์ ๊ทผํ ์ ์์ต๋๋ค
// ํน์ ์คํ์
๊ณผ ์คํธ๋ผ์ด๋๋ฅผ ์ฌ์ฉํ์ฌ.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// ํฌ๋ก๋ง ์ฐจ์์ ์ ๋ฐ์ผ๋ก ์ค์ด๋ญ๋๋ค!
const uPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[1].offset,
layouts[1].stride * (videoFrame.codedHeight / 2)
);
const vPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[2].offset,
layouts[2].stride * (videoFrame.codedHeight / 2)
);
console.log('Accessed Y plane size:', yPlaneView.byteLength);
console.log('Accessed U plane size:', uPlaneView.byteLength);
videoFrame.close();
}
๋ ๋ค๋ฅธ ์ผ๋ฐ์ ์ธ ํ์์ ๋ฐ-ํ๋ฉด(semi-planar)์ธ NV12์
๋๋ค. ์ด๊ฒ์ ๋ ๊ฐ์ ํ๋ฉด์ ๊ฐ์ง๋๋ค: ํ๋๋ Y๋ฅผ ์ํ ๊ฒ์ด๊ณ , ๋ค๋ฅธ ํ๋๋ U์ V ๊ฐ์ด ์ธํฐ๋ฆฌ๋ธ๋(์: [U1, V1, U2, V2, ...]) ํ๋ฉด์
๋๋ค. WebCodecs API๋ ์ด๋ฅผ ํฌ๋ช
ํ๊ฒ ์ฒ๋ฆฌํฉ๋๋ค; NV12 ํ์์ VideoFrame์ ๋จ์ํ layout ๋ฐฐ์ด์ ๋ ๊ฐ์ ๋ ์ด์์์ ๊ฐ์ง๋๋ค.
๊ณผ์ ๋ฐ ๋ชจ๋ฒ ์ฌ๋ก
์ด์ฒ๋ผ ๋ฎ์ ์์ค์์ ์์ ํ๋ ๊ฒ์ ๊ฐ๋ ฅํ์ง๋ง ์ฑ ์์ด ๋ฐ๋ฆ ๋๋ค.
๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ๊ฐ ๊ฐ์ฅ ์ค์ํฉ๋๋ค
VideoFrame์ ์๋นํ ์์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋ณด์ ํ๋ฉฐ, ์ด๋ ์ข
์ข
์๋ฐ์คํฌ๋ฆฝํธ ๊ฐ๋น์ง ์ปฌ๋ ํฐ์ ํ ์ธ๋ถ์์ ๊ด๋ฆฌ๋ฉ๋๋ค. ์ด ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋ช
์์ ์ผ๋ก ํด์ ํ์ง ์์ผ๋ฉด ๋ธ๋ผ์ฐ์ ํญ์ ์ถฉ๋์ํฌ ์ ์๋ ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ๋ฐ์ํฉ๋๋ค.
ํ๋ ์ ์ฌ์ฉ์ด ๋๋๋ฉด ํญ์, ๋ฐ๋์ videoFrame.close()๋ฅผ ํธ์ถํด์ผ ํฉ๋๋ค.
๋น๋๊ธฐ์ ํน์ฑ
๋ชจ๋ ๋ฐ์ดํฐ ์ ๊ทผ์ ๋น๋๊ธฐ์ ์
๋๋ค. ์ ํ๋ฆฌ์ผ์ด์
์ํคํ
์ฒ๋ ๊ฒฝ์ ์กฐ๊ฑด(race condition)์ ํผํ๊ณ ์ํํ ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ์ ๋ณด์ฅํ๊ธฐ ์ํด Promise์ async/await์ ํ๋ฆ์ ์ ์ ํ๊ฒ ์ฒ๋ฆฌํด์ผ ํฉ๋๋ค.
๋ธ๋ผ์ฐ์ ํธํ์ฑ
WebCodecs๋ ์ต์ API์ ๋๋ค. ๋ชจ๋ ์ฃผ์ ๋ธ๋ผ์ฐ์ ์์ ์ง์๋์ง๋ง, ํญ์ ์ฌ์ฉ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ํ์ธํ๊ณ ๊ณต๊ธ์ ์ฒด๋ณ ๊ตฌํ ์ธ๋ถ ์ ๋ณด๋ ์ ํ ์ฌํญ์ ์ธ์งํด์ผ ํฉ๋๋ค. API๋ฅผ ์ฌ์ฉํ๊ธฐ ์ ์ ๊ธฐ๋ฅ ๊ฐ์ง๋ฅผ ์ฌ์ฉํ์ธ์.
๊ฒฐ๋ก : ์น ๋น๋์ค์ ์๋ก์ด ์งํ
WebCodecs API๋ฅผ ํตํด VideoFrame์ ์์ ํ๋ฉด ๋ฐ์ดํฐ์ ์ง์ ์ ๊ทผํ๊ณ ์กฐ์ํ ์ ์๋ ๋ฅ๋ ฅ์ ์น ๊ธฐ๋ฐ ๋ฏธ๋์ด ์ ํ๋ฆฌ์ผ์ด์
์ ๋ํ ํจ๋ฌ๋ค์ ์ ํ์
๋๋ค. ์ด๋ <video> ์์์ ๋ธ๋๋ฐ์ค๋ฅผ ์ ๊ฑฐํ๊ณ ๊ฐ๋ฐ์์๊ฒ ์ด์ ์๋ ๋ค์ดํฐ๋ธ ์ ํ๋ฆฌ์ผ์ด์
์๋ง ์์ฝ๋์๋ ์ธ๋ถํ๋ ์ ์ด๊ถ์ ์ ๊ณตํฉ๋๋ค.
ํ๋ฉด, ์คํธ๋ผ์ด๋, ์์ ํ์๊ณผ ๊ฐ์ ๋น๋์ค ๋ฉ๋ชจ๋ฆฌ ๋ ์ด์์์ ๊ธฐ๋ณธ์ ์ดํดํ๊ณ , ์ฑ๋ฅ์ด ์ค์ํ ์์ ์ ์ํด WebAssembly์ ํ์ ํ์ฉํจ์ผ๋ก์จ, ์ด์ ๋ธ๋ผ์ฐ์ ์์ ์ง์ ๋งค์ฐ ์ ๊ตํ ๋น๋์ค ์ฒ๋ฆฌ ๋๊ตฌ๋ฅผ ๊ตฌ์ถํ ์ ์์ต๋๋ค. ์ค์๊ฐ ์์ ๋ณด์ ๋ฐ ์ปค์คํ ์๊ฐ ํจ๊ณผ์์๋ถํฐ ํด๋ผ์ด์ธํธ ์ธก ๋จธ์ ๋ฌ๋ ๋ฐ ๋น๋์ค ๋ถ์์ ์ด๋ฅด๊ธฐ๊น์ง, ๊ฐ๋ฅ์ฑ์ ๋ฌดํํฉ๋๋ค. ์น์์์ ๊ณ ์ฑ๋ฅ, ์ ์์ค ๋น๋์ค ์๋๊ฐ ์ง์ ์ผ๋ก ์์๋์์ต๋๋ค.